/* ping程序的操作非常简单，往某个IP地址发送一个ICMP回射请求，该节点则以一个ICMP回射应答响应。 */
#include <errno.h>
#include <netdb.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>

/* C99中规定宏可以像函数一样带有可变参数 */
#define error_exit(format, ...) \
	do { fprintf(stderr, format"\n", ##__VA_ARGS__); exit(EXIT_FAILURE); } while (0)

#define	BUFSIZE    1500

char     sendbuf[BUFSIZE];
int      datalen = 56;      // “可选数据”的长度
char    *host;              // 主机名或IP地址数串
int      nsent;             // 发送出去的分组“序列号” 
pid_t    pid;               // 进程ID
int      sockfd;            // 原始套接字描述符
int      verbose;           // "-v"命令行选项是否存在标记

struct proto {
  void (*fproc)(char *, ssize_t, struct msghdr *, struct timeval *);
  void (*fsend)(void);
  struct sockaddr *sasend;	// sockaddr{} for send, from getaddrinfo 
  struct sockaddr *sarecv;	// sockaddr{} for receiving 
  socklen_t salen;		    // length of sockaddr{}s 
  int icmpproto;	        // IPPROTO_xxx value for ICMP 
} *pr;

void* Signal(int signo, void (*func)(int)) {
	struct sigaction act, oact;

	act.sa_handler = func; 
	sigemptyset(&act.sa_mask); 
	act.sa_flags = 0;
#ifdef SA_INTERRUPT	
	if (signo == SIGALRM) act.sa_flags |= SA_INTERRUPT;
#endif
#ifdef SA_RESTART 
	if (signo != SIGALRM) act.sa_flags |= SA_RESTART; 
#endif
	if (sigaction(signo, &act, &oact) < 0) 
		return SIG_ERR;
	return oact.sa_handler; // 返回信号的旧行为
}

struct addrinfo* host_serv(const char *host, const char *serv, int family, int socktype) {
	int n;
	struct addrinfo hints, *res;

	bzero(&hints, sizeof(struct addrinfo));
	hints.ai_flags = AI_CANONNAME;  // always return canonical name 
	hints.ai_family = family;       // 0, AF_INET, AF_INET6, etc
	hints.ai_socktype = socktype;   // 0, SOCK_STREAM, SOCK_DGRAM, etc
	if ((n = getaddrinfo(host, serv, &hints, &res)) != 0) {
		error_exit("host_serv error for %s, %s: %s",
			(host == NULL) ? "(no hostname)" : host,
			(serv == NULL) ? "(no service name)" : serv,
			gai_strerror(n));			 
	}
	return(res);  // return pointer to first on linked list 
}

char* sock_ntop_host(const struct sockaddr *sa, socklen_t salen) {
    static char str[128];   // Unix domain is largest 

	switch (sa->sa_family) {
		case AF_INET: {
			struct sockaddr_in *sin = (struct sockaddr_in *)sa;
			if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
				return(NULL);
			return(str);			
		}
		default:
			snprintf(str, sizeof(str), "sock_ntop_host: unknown AF_xxx: %d, len %d", sa->sa_family, salen);
			return(str);
	}
    return (NULL);
}

/* 相减两个timeval时间，结果放到out中 */
static void tv_sub(struct timeval *out, struct timeval *in) {
	if ((out->tv_usec -= in->tv_usec) < 0) { // out -= in 
		--out->tv_sec;
		out->tv_usec += 1000000;
	}
	out->tv_sec -= in->tv_sec;
}

/* 处理所有接收到的ICMPv4消息。
 * （当一个ICMPv4消息由进程在原始套接字上收取时，内核已经证实它的IPv4首部和ICMPv4首部中的基本字段的有效性） 
 *
 *  |--------------------len-------------------|
 *  |--------hlen1--------|-------icmplen------|
 *  ++++++++++++++++++++++++++++++++++++++++++++
 *  + IPv4  +    IPv4     + ICMPv4 +    ICMP   +
 *  + 首部  +    选项     +  首部  +    数据   +
 *  ++++++++++++++++++++++++++++++++++++++++++++
 *  |--20B--|----0~40B----|---8B---|-----------|
 *  [ip]                  [icmp]
 *
 *  （图：处理ICMPv4应答涉及的首部、指针和长度）
 ************************************************/
void proc_v4(char *ptr, ssize_t len, struct msghdr *msg, struct timeval *tvrecv) {
	int hlen1, icmplen;
	double rtt;
	struct ip *ip;
	struct icmp *icmp;
	struct timeval *tvsend;

	ip = (struct ip *)ptr;        // start of IP header 
	hlen1 = ip->ip_hl << 2;       // length of IP header 
	if (ip->ip_p != IPPROTO_ICMP) // 检查ICMP标识符字段
		return;	// not ICMP 

	icmp = (struct icmp *)(ptr + hlen1);  // start of ICMP header 
	if ((icmplen = len - hlen1) < 8)      // 是否为完整ICMP数据
		return; // malformed packet 

	if (icmp->icmp_type == ICMP_ECHOREPLY) { // ICMP消息的“类型”
		if (icmp->icmp_id != pid)            // ICMP消息的“标识符”
			return; // not a response to our ECHO_REQUEST 
		if (icmplen < 16)
			return; // not enough data to use 

		tvsend = (struct timeval *)icmp->icmp_data; // ICMP消息的“可选数据”
		tv_sub(tvrecv, tvsend);
		rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0; // 计算RTT时间
		printf("%d bytes from %s: seq=%u, ttl=%d, rtt=%.3f ms\n",
				icmplen, sock_ntop_host(pr->sarecv, pr->salen),
				icmp->icmp_seq, ip->ip_ttl, rtt);
	} 
	
	/* 若指定-v（详尽输出）则显示所有接收ICMP消息 */
	if (verbose) { 
		printf("  %d bytes from %s: type = %d, code = %d\n",
				icmplen, sock_ntop_host(pr->sarecv, pr->salen),
				icmp->icmp_type, icmp->icmp_code);
	}
}

/* 计算网际网校验和（具体是指被校验的各个16位值的二进制反码和） */
static uint16_t in_cksum(uint16_t *addr, int len) {
	int nleft = len;
	uint32_t sum = 0;
	uint16_t *w = addr;
	uint16_t answer = 0;

	/* Our algorithm is simple, using a 32 bit accumulator (sum), we add
	 * sequential 16 bit words to it, and at the end, fold back all the
	 * carry bits from the top 16 bits into the lower 16 bits. */
	while (nleft > 1)  {
		sum += *w++;
		nleft -= 2;
	}
	if (nleft == 1) {
		*(unsigned char *)(&answer) = *(unsigned char *)w ;
		sum += answer;
	}

	/* add back carry outs from top 16 bits to low 16 bits */
	sum = (sum >> 16) + (sum & 0xffff); // sum的高16位与低16位第一次相加
	sum += (sum >> 16);                 // 将上一步可能产生的高16位进位再次与低16位累加  
	answer = ~sum;                      // truncate to 16 bits 
	return(answer);
}


/* 发送ICMPv4回射请求消息。 
 *
 * 0_______7.8_____15.16_____________31
 * |__类型__|__代码__|_____校验和_____|
 * |______标识符_____|_____序列号_____|
 * |                                  |
 * .             可选数据             .
 * |__________________________________|
 *
 *        （图：ICMPv4消息的格式）
 ************************************************/
void send_v4(void) {
	int len;
	struct icmp *icmp;

	/* 构造ICMPv4消息 */
	icmp = (struct icmp *)sendbuf;
	icmp->icmp_type = ICMP_ECHO;    // 消息的“类型”
	icmp->icmp_code = 0;            // 消息的“代码”值为0
	icmp->icmp_id = pid;            // 消息的“标识符”使用ping进程的PID
	icmp->icmp_seq = nsent++;       // 消息的“序列号”递增
	memset(icmp->icmp_data, 0xa5, datalen);                // 消息的“可选数据”填充0xa5
	gettimeofday((struct timeval *)icmp->icmp_data, NULL); // 消息的“可选数据”开始处存放发送时刻的8字节时间戳

	/* 计算ICMP校验和 */
	len = 8 + datalen;              // ICMP“首部”和“可选数据”的长度和
	icmp->icmp_cksum = 0;           // 计算校验和之前，要将校验和字段置为0
	icmp->icmp_cksum = in_cksum((u_short *)icmp, len);

	/* 发送数据报（由于没有开启IP_HDRINCL选项，内核将构造IPv4首部并安置在上述ICMPv4消息缓冲区之前） */
	sendto(sockfd, sendbuf, len, 0, pr->sasend, pr->salen);
}

/* 每隔1s发送一个ICMP回射请求 */
static void sig_alrm(int signo) {
	(*pr->fsend)();

	alarm(1); 
	return;
}

void readloop(void) {
	int size;
	char recvbuf[BUFSIZE];
	char controlbuf[BUFSIZE];
	struct msghdr msg;
	struct iovec iov;
	ssize_t n;
	struct timeval tval;

	/* 创建原始套接字（需超级用户特权） */
	sockfd = socket(pr->sasend->sa_family, SOCK_RAW, pr->icmpproto); // 进程必须拥有超级用户特权才能创建原始套接字
	setuid(getuid()); // 把进程的有效用户ID设置为实际用户ID，使得进程放弃对超级用户特权的拥有（防攻击）

	/* 设置套接字接收缓冲区的大小，防止接收缓冲区溢出 */
	size = 60 * 1024; 
	setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));

	/* 启动回射请求，由SIGALRM信号每秒钟驱动一次 */
	sig_alrm(SIGALRM);  

	/* 在一个原始套接字上读入收到的每个分组，显示ICMP回射应答 */
	iov.iov_base = recvbuf;
	iov.iov_len = sizeof(recvbuf);
	msg.msg_name = pr->sarecv;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = controlbuf;
	for ( ; ; ) {
		msg.msg_namelen = pr->salen;
		msg.msg_controllen = sizeof(controlbuf);
		n = recvmsg(sockfd, &msg, 0);  // 读入回射到原始ICMP套接字的每个分组
		if (n < 0) {
			if (errno == EINTR)
				continue;
			else 
				error_exit("recvmsg(%d)=%d", sockfd, n);
		}

		gettimeofday(&tval, NULL);    // 记录分组收取时刻，用于计算RTT
		(*pr->fproc)(recvbuf, n, &msg, &tval);
	}
}

int main(int argc, char **argv) {
	struct proto proto_v4 = { proc_v4, send_v4, NULL, NULL, 0, IPPROTO_ICMP };	// IPv4的proto结构
	int c;
	struct addrinfo	*ai;
	char *h;

	/* getopt被用来解析命令行选项参数。调用一次，返回一个选项。
	 * extern int opterr;   //当opterr=0时，getopt不向stderr输出错误信息
	 * extern int optopt;   //未知选项存储在optopt中，并且getopt返回'?'
	 * extern int optind;   //getopt要处理的argv中的下一个字符选项的索引
	 * extern char *optarg; //选项的参数指针	 
	 * 当再也检查不到包含的选项时，getopt返回-1，同时optind储存第一个不包含选项的命令行参数的索引。*/
	opterr = 0; 
	while ((c = getopt(argc, argv, "v")) != -1) { // 此程序只有-v选项
		switch (c) {
			case 'v':        // -v选项
				verbose++;
				break;
			case '?':        // 未知选项
				error_exit("unrecognized option: %c", optopt);
		}
	}
	if (optind != argc-1) 
		error_exit("usage: ping [ -v ] <hostname>");	

	host = argv[optind];       // <hostname> 主机名或IP地址数串
	pid = getpid() & 0xffff;   // ICMP ID field is 16 bits 
	Signal(SIGALRM, sig_alrm);

	ai = host_serv(host, NULL, 0, 0);                // 处理主机名或IP地址数串，返回addrinfo结构中含有协议族AF_INET等信息
	h = sock_ntop_host(ai->ai_addr, ai->ai_addrlen); // 从套接字地址结构中得到主机IP信息
	printf("PING %s (%s): %d data bytes\n", ai->ai_canonname ? ai->ai_canonname : h, h, datalen);		
	if (ai->ai_family == AF_INET) 
		pr = &proto_v4;
	else 
		error_exit("unknown address family %d", ai->ai_family);		

	pr->sasend = ai->ai_addr;
	pr->sarecv = calloc(1, ai->ai_addrlen);
	pr->salen = ai->ai_addrlen;

	readloop();

	free(pr->sarecv);
	freeaddrinfo(ai);
	exit(0);
}



